package kernel;

import java.awt.EventQueue;
import java.io.File;
import java.io.UnsupportedEncodingException;
import java.lang.Thread.State;
import java.net.URLDecoder;
import java.security.InvalidParameterException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ArrayBlockingQueue;

import program.Shell;

import kernel.exception.OSException;
import kernel.process.AbstractProcessHeader;

import shell.ShellWindow;

/**
 * Class responsible for IO operations with shell window
 * @author Jaromir Lepic
 *
 */
public class IOManager {
	private static IOManager instance;
	public static final long STARTING_SHELL_ID = 0;
	private ArrayBlockingQueue<AbstractProcessHeader[]> messages = new ArrayBlockingQueue<AbstractProcessHeader[]>(10);
	private Map<Long, ShellWindow> clients = new HashMap<Long, ShellWindow>();
	private Map<Long, List<Shell>> shells = new HashMap<Long, List<Shell>>();
	private long newID = STARTING_SHELL_ID;
	
	private IOManager(){
	}
	
	/**
	 * Checks if message queue with AbstractProcessHeaders is empty
	 * @return True if empty
	 */
	public boolean isMessageQueueEmpty(){
		return messages.isEmpty();
	}
	
	/**
	 * Gets list of shells with given ID
	 * @param shellID ID of shells to be returned
	 * @return List of found shells
	 */
	public List<Shell> getShells(long shellID){
		return shells.get(shellID);
	}
	
	/**
	 * Gets the last (= active) shell from shells with given ID
	 * @param shellID ID of shell to be returned
	 * @return Found shell
	 */
	public Shell getActiveShell(long shellID){
		List<Shell> list = shells.get(shellID);
		return list.get(list.size()-1);
	}
	
	/**
	 * Requests that window enables standard input and then sends it back
	 * @param shellID ID of window to enable standard input
	 * @param dispatcher Dispatcher of process that requests the input
	 * @return Complete input from window
	 */
	public String requestSTDInputText(long shellID, final Thread dispatcher){
		final StringBuffer messageBuffer = new StringBuffer("");
		if (shells.containsKey(shellID)){
			final ShellWindow window = clients.get(shellID);
			EventQueue.invokeLater(new Runnable(){

				@Override
				public void run() {
					window.getTextFromShell(messageBuffer, dispatcher);
				}
				
			});
		}
		synchronized (dispatcher) {
			try {
				dispatcher.wait();
			} catch (InterruptedException e) {
			}
		}
		return messageBuffer.toString();
	}
	
	/**
	 * Requests that window enables standard input of one line and then sends it back
	 * @param message Message to be printed before input is enabled
	 * @param shellID ID of window to enable standard input
	 * @param dispatcher Dispatcher of process that requests the input
	 * @param hidden True if input should be printed as stars
	 * @return Line from window
	 */
	public String requestSTDInputLine(final String message, long shellID, final Thread dispatcher, final boolean hidden){
		final StringBuffer messageBuffer = new StringBuffer("");
		if (shells.containsKey(shellID)){
			final ShellWindow window = clients.get(shellID);
			EventQueue.invokeLater(new Runnable(){

				@Override
				public void run() {
					window.getLineFromShell(messageBuffer, message, dispatcher, hidden);
				}
				
			});
		}
		synchronized (dispatcher) {
			try {
				dispatcher.wait();
			} catch (InterruptedException e) {
			}
		}
		return messageBuffer.toString();
	}
	
	/**
	 * Sets the user of currently active shell with given ID
	 * @param shellID ID of the shell
	 * @param user User to be set
	 */
	public void setShellUser(long shellID, String user){
		List<Shell> list = shells.get(shellID);
		if (user == null) return;
		if (list == null) return;
		Shell shell = list.get(list.size() -1);
		shell.setUserID(user);
		clients.get(shellID).setUserID(user);
	}
	
	/**
	 * Shell window is closing and needs to be removed from IOManager
	 * @param ID ID of shell window
	 * @param manually True if the dispose method was called manually from code
	 */
	public void shellClosing(long ID, boolean manually){
		if (!manually)
			while (shells.get(ID)!= null && !shells.get(ID).isEmpty()){
				try {
					shells.get(ID).iterator().next().killProcess();
				} catch (OSException e) {
					e.printStackTrace();
				}
			}
	}
	
	/**
	 * Polls the AbstractProcessHeaders message queue
	 * @return the poll
	 */
	public AbstractProcessHeader[] pollMessageQueue(){
		return messages.poll();
	}
	
	/**
	 * Gets next ShellID to be assigned
	 * @return next ShellID 
	 */
	public long getNextShellID(){
		if (newID == STARTING_SHELL_ID)
			return newID++;
		else return newID++;
			
	}
	
	/**
	 * Gets the instance of IOManager
	 * @return singleton instance
	 */
	public static IOManager getInstance(){
		if (instance == null){
			instance = new IOManager();
		}
		return instance;
	}
	
	/**
	 * Registers new shell to IOManager under given ID
	 * @param shell Shell to be registered
	 * @param shellID ShellID to be used
	 */
	public void registerShell(Shell shell, long shellID){
		if (shells.containsKey(shellID)){
			List<Shell> list = shells.get(shellID);
			list.add(shell);
		} else {
			List<Shell> list =  new ArrayList<Shell>();
			list.add(shell);
			shells.put(shellID, list);
		}
		ShellWindow shellWindow = clients.get(shellID);
		shell.setShellWindow(shellWindow);
		shell.setID(shellID);
		shell.getThread().start();
	}
	
	/**
	 * Removes the given shell from IOManager under given ID. Terminates window if last shell
	 * @param shell Shell to be removed
	 * @param shellID ShellID to be used
	 */
	public void unregisterShell(Shell shell, long shellID){
		List<Shell> list = shells.get(shellID);
		if (shell == null) return;
		if (list == null) return;
		int index = list.indexOf(shell);
		ShellWindow shellWindow = clients.get(shellID);
		if (index > -1){
			if (index == 0 && list.size() == 1){
				shellWindow.disposeManually();
				clients.remove(shellID);
				shells.remove(shellID);
			} else if (index == list.size()-1){
				list.get(list.size()-2).setShellWindow(shellWindow);
			}
			list.remove(index);
		}
		if (shells.isEmpty()){
			ProcessManager.getInstance().shutdown();
		}
	}
	
	/**
	 * Requests a message to be send to shell window with given ID
	 * @param shellID ShellID of target window
	 * @param message Message to be sent
	 * @param lastMessage If working directory should be printed after the message
	 * @param senderToNotify If the sender thread waits for notification
	 * @throws InvalidParameterException
	 */
	public void sendMessageToClient(final long shellID, final String message, final boolean lastMessage, final Thread senderToNotify ) throws InvalidParameterException{
		if (!shells.containsKey(shellID)) return;
		if (senderToNotify == null){
			synchronized (shells) {
				if (shells.containsKey(shellID)){
					List<Shell> list = shells.get(shellID);
					Shell active = list.get(list.size()-1);
					active.setMessageToWindow(message, lastMessage, null);
					synchronized (active.getThread()) {
						if (active.getThread().getState().equals(State.WAITING))
							active.getThread().notify();
					}
				}
			}
		} else {
			final Thread thread = new Thread() {
				@Override
				public void run() {
					synchronized (shells) {
						if (shells.containsKey(shellID)){
							List<Shell> list = shells.get(shellID);
							Shell active = list.get(list.size()-1);
							active.setMessageToWindow(message, lastMessage, this);
							synchronized (active.getThread()) {
								if (active.getThread().getState().equals(State.WAITING))
									active.getThread().notify();
							}
						}
					}
					try {
						synchronized (this) {
							if (!interrupted())
								this.wait();
						}
					} catch (InterruptedException e) {
					}
				}
				
			};
			thread.start();
			try {
				thread.join();
			} catch (InterruptedException e) {
			}
		}
	}
	
	/**
	 * Sets the working directory of all shells with given ID
	 * @param shellID ID to be used
	 * @param newWorkingDir New working directory
	 */
	public void setWorkingDirectory(long shellID, File newWorkingDir){
		for (Shell shell: shells.get(shellID)){
			shell.setNewWorkingDirectory(newWorkingDir);
		}
	}
	
	/**
	 * Gets the working directory of shells with given ID
	 * @param shellID ID to be used
	 * @return Working directory of shells
	 */
	public File getWorkingDirectory(long shellID){
		if (shells.isEmpty()){
			File f = null;
			try {
				f = new File(URLDecoder.decode(IOManager.class.getProtectionDomain().getCodeSource().getLocation().getPath(), "UTF-8"));
			} catch (UnsupportedEncodingException e) {
				e.printStackTrace();
			}
			return f;
		}
		else
			return shells.get(shellID).get(0).getWorkingDirectory();
	}
	
	/**
	 * Creates new shell window conected to given shell under new shellID
	 * @param shell Shell to be connected
	 * @param shellID ShellID to be used
	 * @param oldShellID Not used... whatever
	 * @return Thread that handles the new shell window
	 */
	public Thread createShellWindow(final Shell shell, final long shellID, final long oldShellID){
		List<Shell> list =  new ArrayList<Shell>();
		list.add(shell);
		shells.put(shellID, list);
		shell.setID(shellID);
		Thread r = new Thread() {
			public void run() {
				ShellWindow shellWindow = new ShellWindow(shell.getWorkingDirectory(), this);
				shell.setShellWindow(shellWindow);
				shell.getThread().start();
				clients.put(shellID, shellWindow);
			}
		};
		EventQueue.invokeLater(r);
		return r;
	}
	
	/**
	 * Closs all shell windows
	 */
	public void shutdownWindows(){
		for (ShellWindow SW: clients.values()){
			SW.disposeManually();
		}
	}

}
